Skip to content

S14-01 SSR-Vue3 SSR

[TOC]

概念

SEO

概述

SEO(Search Engine Optimization,搜索引擎优化)是通过优化网站结构、内容和外部链接,提高网站在搜索引擎结果页(SERP)中的排名,从而增加网站流量的技术和策略。

优化策略:

  • 语义性HTML标记:

    • 标题用<h1>,一个页面只有一个; 副标题用<h2><h6>。不要过度使用h标签,多次使用不会增加 SEO。

    • 段落用<p>

    • 列表用<ul>,并且li只放在 ul 中。

  • 每个页面需包含:标题+内部链接: 每个页面对应的title,同一网站所有页面都有内链可以指向首页。

  • 确保链接可供抓取:

    image-20240919163211380

  • meta标签优化: 设置 description keywords等。

  • 文本标记和img:

    • 比如使用<b><strong>加粗文本的标签,爬虫也会关注到该内容。
    • img标签添加 alt 属性,图片加载失败,爬虫会取alt内容。
  • robots.txt文件: 规定爬虫可访问您网站上的哪些网址。robots文件生成

  • sitemap.xml站点地图: 在站点地图列出所有网页,确保爬虫不会漏掉某些网页。

  • 更多:https://developers.google.com/search/docs/crawling-indexing/valid-page-metadata

image-20240919162937328

爬虫-工作流程

Google爬虫的工作流程分为3个阶段,并非每个网页都会经历这3个阶段

  • 抓取网页: 爬虫(蜘蛛),从互联网上发现各类网页,网页中的外部连接也会被发现。
  • 索引编制: 爬虫程序会分析网页上的文本、图片和视频文件,并将信息存储在大型数据库(索引区)中。爬虫会对内容类似的网页归类分组,不符合规则内容和网站会被清理,如禁止访问或需要权限的网站等。
  • 搜索结果: 当用户在Google中搜索时,搜索引擎会根据内容类型,选择一组网页中最具代表性的网页进行呈现。

image-20240911154151286

CSR

概述

CSR(Client-Side Rendering,客户端渲染)是一种网页渲染方式,主要依靠浏览器在用户设备上生成HTML内容。应用程序的JS代码在客户端执行,从而动态生成页面

客户端渲染原理:

image-20240911154139238

SPA

SPA(Single Page Application,单页面应用)是一种Web应用架构,属于CSR,它通过在单个HTML页面上动态加载内容,实现流畅的用户体验。与传统的多页面应用不同,SPA在用户与应用交互时,只更新部分内容,而无需重新加载整个页面

特点:

  • SPA属于CSR(Client-Side Rendering,客户端渲染)
  • SPA应用默认只返回一个空HTML页面,如<div id="app"></div>
  • 整个应用程序的内容都是通过 Javascript 动态加载,包括应用程序的逻辑、UI 以及与服务器通信相关的所有数据。
  • 构建 SPA 应用常见的库和框架有: React、AngularJS、Vue.js 等。

SPA优点:

  • 流畅性:用户体验更佳,页面切换更快。
  • 减轻服务器负担:只请求必要的数据,而非整个页面。
  • 前后端分离:可以更容易地进行前后端开发协作。

SPA缺点:

  • 不利于SEO:由于内容是在客户端渲染的,搜索引擎可能难以索引。
  • 首屏加载时间:可能比多页面应用稍慢,因为需要下载和解析JavaScript。
  • 浏览器历史管理:需要处理浏览器的前进和后退按钮。
  • 不利于构建复杂项目: 复杂web应用的大文件难以维护。

SSG

SSG(Static Site Generator,静态站点生成器)是一种工具,用于根据预定义的模板和内容生成静态HTML文件的过程。常用于构建网站博客文档

特点:

  • SSG 应用一般在构建阶段就确定了网站的内容。
  • 如果网站的内容需要更新了,那必须得重新再次构建和部署。
  • 构建 SSG 应用常见的库和框架有: Vue NuxtReact Next.js 等。

SSG优点:

  • 性能优越: 访问速度非常快,因为每个页面都是在构建阶段就已经提前生成好了。
  • SEO友好: 直接给浏览器返回静态的HTML,也有利于SEO。
  • 保留SPA特性: SSG应用依然保留了SPA应用的特性,比如:前端路由、响应式数据、虚拟DOM等。

SSG缺点:

  • 动态内容限制:页面都是静态,不利于展示实时性的内容,实时性的更适合SSR。
  • 更新复杂性:如果站点内容更新了,那必须得重新再次构建和部署。
  • 构建时间:对于大型网站,生成所有静态页面可能需要较长时间。

SSR

概述

SSR(Server-Side Rendering,服务器端渲染)是一种用于提高Web应用性能和SEO优化的技术。SSR在服务器上生成HTML页面并将其发送到客户端。

特点:

  • SSR应用的页面是在服务端渲染的,用户每请求一个SSR页面都会先在服务端进行渲染,然后将渲染好的页面,返回给浏览器呈现。
  • 构建 SSR 应用常见的库和框架有: Vue NuxtReact Next.js 等(SSR应用也称同构应用) 。

服务器端渲染原理:

1、通过node将vue项目打包成静态html页面

image-20240919171511224

2、激活应用程序,实现交互(Hydration)

image-20240919172125393

image-20240911154224018

SSR优点:

  • 更快的首屏渲染速度

    • 浏览器显示静态页面的内容要比 JavaScript 动态生成的内容快得多。
    • 当用户访问首页时可立即返回静态页面内容,而不需要等待浏览器先加载完整个应用程序。
  • 更好的SEO

    • 爬虫是最擅长爬取静态的HTML页面,服务器端直接返回一个静态的HTML给浏览器。
    • 这样有利于爬虫快速抓取网页内容,并编入索引,有利于SEO。
  • 保留Web交互性: SSR应用程序在 Hydration 之后依然可以保留 Web 应用程序的交互性。比如:前端路由、响应式数据、虚拟DOM等。

SSR缺点:

  • 服务器负担:SSR需要对服务器进行更多 API 调用,在服务器端渲染需要消耗更多的服务器资源,成本高。
  • 增加开发成本:用户需要关心哪些代码是运行在服务器端,哪些代码是运行在浏览器端。
  • 复杂性:SSR 配置站点的缓存通常会比SPA站点要复杂一点。

SSR解决方案

SSR解决方案:

  • 方案一:php、jsp等

  • 方案二:从零搭建SSR 项目( Node+webpack+Vue/React )

  • 方案三:直接使用流行的框架(推荐

    • React : Next.js(127K)、Remix.js(29.8K)

    • Vue3 : Nuxt3(54.7K)

    • Vue2 : Nuxt.js

    • Angular : Anglular Universal

      image-20240911154253222

SSR应用场景:

  • 性能要求高的系统:移动端、弱网环境
  • 操作交互简单的系统:
    • SaaS产品,如:电子邮件网站、在线游戏、客户关系管理系统(CRM)、采购系统等
    • 门户网站、电子商务、零售网站
    • 单个页面、静态网站、文档类网站

image-20240911154259643

原生实现

API

vue

  • createSSRApp()(rootComponent), Vue 3 中用于创建服务端渲染(SSR)应用的函数。它通常在 Vue 3 的 SSR 相关环境中使用,如 @vue/server-renderer

    • rootComponentComponent,应用的根组件,通常是一个 Vue 组件对象。通常为App组件。

    • 返回:

    • appApp,SSR 应用实例。

      js
      // server.js
      import { createSSRApp } from 'vue';
      import { renderToString } from '@vue/server-renderer';
      import MyComponent from './MyComponent.vue';
      
      async function createApp() {
        const app = createSSRApp(MyComponent);
        return app;
      }
      
      async function render() {
        const app = await createApp();
        const html = await renderToString(app);
        return html;
      }
      
      render().then((html) => {
        console.log(html); // 输出生成的 HTML 字符串
      });

@vue/server-renderer

  • renderToString()(app),将 Vue 组件渲染为 HTML 字符串。

    • appComponent,需要渲染的 Vue 应用实例,可以是一个组件或一个 Vue 应用。

    • 返回:

    • promiseresovle,reject,返回一个 Promise,解析为包含组件的 HTML 字符串。

      • resovle(htmlString) => void,参数为html字符串。
      • reject(err) => void
      js
      import { createSSRApp } from 'vue';
      import { renderToString } from '@vue/server-renderer';
      import Greeting from './Greeting.vue';
      
      const app = createSSRApp(Greeting, { name: 'World' });
      
      // 使用 renderToString 渲染为 HTML 字符串
      renderToString(app).then(htmlString => {
        console.log(htmlString); // 输出: <h1>Hello, World!</h1>
      }).catch(err => {
        console.error('渲染出错:', err);
      });

webpack-node-externals

webpack-node-externals:是一个 Webpack 插件,主要用于在打包 Node.js 应用时,自动排除 node_modules 目录中的所有依赖,以避免将它们打包到输出文件中。

注意:

  • 对于服务器端代码(如 Express 应用)特别有用,因为 Node.js 会在运行时直接从 node_modules 中加载这些依赖,而不需要将它们打包。
  • 对于在浏览器中使用的代码,需要在配置中指定不排除它们。

配置:

js
// webpack.config.js

const nodeExternals = require('webpack-node-externals');

module.exports = {
  target: 'node', // 指定打包目标为 Node.js
  externals: [nodeExternals()], // 使用插件排除 node_modules
  // 其他配置...
};

Vue3 SSR

Vue除了支持开发SPA应用之外,其实也是支持开发SSR应用的。

在Vue中创建SSR应用,需要调用createSSRApp函数,而不是createApp

  • createApp:创建应用,直接挂载到页面上
  • createSSRApp:创建应用,是在激活的模式下挂载应用

服务端用 @vue/server-renderer 包中的 renderToString 来进行渲染。

image-20240911154312608

image-20240911154317705

实现过程

实现思路:

  • 1 开发一个App应用,比如App.vue。
  • 2 将App.vue打包为一个服务器端的server_bundle.js文件,用来在服务器端动态生成页面的HTML。
  • 3 将App.vue打包为一个客户端的client_bundle.js文件,用来激活应用,使页面有交互效果。
  • 4 server_bundle.js 渲染的页面 + client_bundle.js 文件进行Hydration。

搭建Node Server

依赖包:

  • express
    • 安装:pnpm i express
  • nodemon:启动Node程序时并监听文件的变化,变化即刷新。
    • 安装:pnpm i nodemon -D
  • webpackwebpack-cli
    • 安装:pnpm i webpack webpack-cli -D
  • webpack-node-externals:打包时排除 node_modules 中所有的模块。
    • 安装:pnpm i webpack-node-externals -D

搭建过程:

1、运行pnpm init 初始化package.json

image-20240919175946214

2、创建一个express服务器

image-20240919180232957

3、在package.json中编写npm脚本,运行node服务器

image-20240919180422001

image-20240919180459299

4、打包/src/server/index.js文件

  • 打包配置/config/wk.config.jstarget

    image-20240925121359802

  • 打包命令,--watch表示内容变化时会重新打包。

    image-20240925121315080

5、优化打包:使用 webpack-node-externals 在打包时排除node_modules中的包。

此时打包的js文件有900kb大小,需要优化。

image-20240925121711211

6、测试打包后的js文件是否可以运行(OK)

image-20240925122002713

image-20240925122133708

搭建Vue3 SSR Server

依赖包:

  • vuevue-loader
    • 安装:pnpm i vue
    • 安装:pnpm i vue-loader -D
  • babel-loader@babel/preset-env
    • 安装:pnpm i babel-loader @babel/preset-env -D

搭建过程:

1、编写App.vue

image-20240925123718658

2、配置解析后缀名

image-20240925125513703

3、在src/app.js中通过createSSRApp()创建一个App实例。

image-20240925124316023

4、在src/server/index.js中通过renderToString()方法将app实例转化为HTML字符串。并返回给前端

image-20240925124824685

5、重命名wp.config.jsserver.config.js并修改配置。

image-20240925125257140

6、打包项目:pnpm run build:server

7、运行打包后的项目:pnpm run start

image-20240925125633453

image-20240925125645636

8、此时页面可以展示,但是不能互动,页面中的按钮不起作用。

Hydration

服务器端渲染页面 + 客户端激活页面,是页面有交互效果(这个过程称为:Hydration 水合)

Hydration的具体步骤如下:

1、在src/client/index.js中通过createApp()创建一个App实例并挂载到#app元素上。

image-20240925150920776

2、创建配置文件client.config.js,并配置打包项。

image-20240925150801487

3、创建打包脚本

image-20240925150839621

4、打包项目:pnpm run build:client

image-20240925150958892

5、在src/server/index.js的HTML模板中,引入client_bundle.js。JS文件部署在静态服务器中。

image-20240925151828000

6、运行项目:pnpm run start。此时页面中的JS代码已经激活。

image-20240925152001583

问题:

image-20240925152435744

解决:取消这2个常量的打包

image-20240925152636070

合并配置

依赖包:

  • webpack-merge:合并webpack配置。
    • 安装:pnpm i webpack-merge -D

base.config.js

image-20240925155633771

server.config.js

image-20240925155910436

client.config.js

image-20240925155946098

js
const path = require('path')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base')
const { merge } = require('webpack-merge')

/**
 * @type {import('webpack').Configuration}
 */
module.exports = merge(baseConfig, {
  target: 'node',
  entry: './src/server/index.js',
  output: {
    filename: 'server_bundle.js',
    path: path.resolve(__dirname, '../build/server')
  },
  externals: [nodeExternals()]
})
js
const path = require('path')
const { DefinePlugin } = require('webpack')
const baseConfig = require('./webpack.base')
const { merge } = require('webpack-merge')

/**
 * @type {import('webpack').Configuration}
 */
module.exports = merge(baseConfig, {
  target: 'web',
  entry: './src/client/index.js',
  output: {
    filename: 'client_bundle.js',
    path: path.resolve(__dirname, '../build/client')
  },
  plugins: [
    new DefinePlugin({
      __VUE_OPTIONS_API__: true,
      __VUE_PROD_DEVTOOLS__: false,
      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
    })
  ]
})
js
const { VueLoaderPlugin } = require('vue-loader')

/**
 * @type {import('webpack').Configuration}
 */
module.exports = {
  mode: 'development',
  devtool: false,
  output: {
    clean: true
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader']
      },
      {
        test: /\.s[ac]ss$/,
        use: ['style-loader', 'css-loader', 'sass-loader']
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, '../src')
    },
    extensions: ['.js', '.json', '.vue']
  }
}

集成Vue Router

依赖包:

  • vue-router
    • 安装:pnpm i vue-router

实现过程:

1、在src/router/index.js中创建一个路由实例。

注意: 为了避免 跨请求状态污染,需在每个请求中都通过函数创建一个全新router。

image-20240925161120443

2、在src/server/index.js中挂载路由到app上。

image-20240925161950101

3、在src/client/index.js中也挂载一遍路由。

image-20240925162201935

4、在src/App.vue中添加路由占位。

image-20240925161759482

5、效果

image-20240925163514054

集成Pinia

依赖包:

  • pinia
    • 安装:pnpm i pinia

实现过程:

1、在src/store/index.js中创建一个pinia实例。

注意: 为了避免 跨请求状态污染,需在每个请求中都通过函数创建一个全新store。

image-20240925165233835

2、在src/server/index.js中挂载pinia到app上。

image-20240925165429904

3、在src/client/index.js中也挂载一遍pinia。

image-20240925165442219

4、在组件中使用store

image-20240925165754142

跨请求状态污染

跨请求状态污染(Cross-Request State Pollution)是指在 Web 应用中,多个请求之间的状态意外共享或干扰,导致数据不一致或安全问题。

产生原因:

  • SPA环境中,每个用户在使用浏览器访问SPA应用时,应用模块都会重新初始化,这是一种单例模式。因此整个生命周期中只有一个App对象实例一个Router对象实例一个Store对象实例

  • SSR环境中,App应用模块通常只在服务器启动时初始化一次。同一个应用模块会在多个服务器请求之间被复用,而我们的单例状态对象也一样,也会在多个请求之间被复用。当某个用户对共享的单例状态进行修改,那么这个状态可能会意外地泄露给另一个在请求的用户。

解决方案:

  • 在每个请求中为整个App应用通过函数创建一个全新的实例,包括后面的 router 和全局 store等实例。

缺点:

  • 需要消耗更多的服务器的资源,因为通过函数创建的实例都是保存在服务器端。